msg_tool\scripts\ex_hibit\arc/
grp.rs

1//! ExHibit GRP archive extractor.
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::{Context, Result};
6use std::fmt::Debug;
7use std::io::{Read, Seek, SeekFrom};
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10
11#[derive(Debug)]
12/// Builder for ExHibit GRP archives.
13pub struct ExHibitGrpArchiveBuilder {}
14
15impl ExHibitGrpArchiveBuilder {
16    /// Creates a new builder instance.
17    pub const fn new() -> Self {
18        Self {}
19    }
20}
21
22impl ScriptBuilder for ExHibitGrpArchiveBuilder {
23    fn default_encoding(&self) -> Encoding {
24        Encoding::Cp932
25    }
26
27    fn default_archive_encoding(&self) -> Option<Encoding> {
28        Some(Encoding::Cp932)
29    }
30
31    fn build_script(
32        &self,
33        data: Vec<u8>,
34        filename: &str,
35        _encoding: Encoding,
36        archive_encoding: Encoding,
37        config: &ExtraConfig,
38        _archive: Option<&Box<dyn Script>>,
39    ) -> Result<Box<dyn Script + Send + Sync>> {
40        Ok(Box::new(ExHibitGrpArchive::new(
41            MemReader::new(data),
42            filename,
43            archive_encoding,
44            config,
45        )?))
46    }
47
48    fn build_script_from_file(
49        &self,
50        filename: &str,
51        _encoding: Encoding,
52        archive_encoding: Encoding,
53        config: &ExtraConfig,
54        _archive: Option<&Box<dyn Script>>,
55    ) -> Result<Box<dyn Script + Send + Sync>> {
56        if filename == "-" {
57            return Err(anyhow::anyhow!(
58                "Reading ExHibit GRP from stdin is not supported; provide a file path."
59            ));
60        }
61        let file = std::fs::File::open(filename)
62            .with_context(|| format!("Failed to open '{}'.", filename))?;
63        let reader = std::io::BufReader::new(file);
64        Ok(Box::new(ExHibitGrpArchive::new(
65            reader,
66            filename,
67            archive_encoding,
68            config,
69        )?))
70    }
71
72    fn build_script_from_reader<'a>(
73        &self,
74        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
75        filename: &str,
76        _encoding: Encoding,
77        archive_encoding: Encoding,
78        config: &ExtraConfig,
79        _archive: Option<&Box<dyn Script>>,
80    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
81        Ok(Box::new(ExHibitGrpArchive::new(
82            reader,
83            filename,
84            archive_encoding,
85            config,
86        )?))
87    }
88
89    fn extensions(&self) -> &'static [&'static str] {
90        &["grp"]
91    }
92
93    fn script_type(&self) -> &'static ScriptType {
94        &ScriptType::ExHibitGrp
95    }
96
97    fn is_this_format(&self, filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
98        if !matches_grp_name(filename) {
99            return None;
100        }
101        if buf_len >= 4 && buf.starts_with(b"AiFS") {
102            return None;
103        }
104        Some(10)
105    }
106
107    fn is_archive(&self) -> bool {
108        true
109    }
110}
111
112#[derive(Clone, Debug)]
113struct GrpFileEntry {
114    name: String,
115    offset: u64,
116    size: u64,
117}
118
119#[derive(Debug)]
120/// ExHibit GRP archive instance.
121pub struct ExHibitGrpArchive<'b, T: Read + Seek + Debug + 'b> {
122    reader: Arc<Mutex<T>>,
123    entries: Vec<GrpFileEntry>,
124    _mark: std::marker::PhantomData<&'b ()>,
125}
126
127impl<'b, T: Read + Seek + Debug + 'b> ExHibitGrpArchive<'b, T> {
128    fn new(
129        mut reader: T,
130        filename: &str,
131        _archive_encoding: Encoding,
132        _config: &ExtraConfig,
133    ) -> Result<Self> {
134        let mut header = [0u8; 4];
135        reader
136            .peek_exact_at(0, &mut header)
137            .context("Failed to read GRP header.")?;
138        if &header == b"AiFS" {
139            return Err(anyhow::anyhow!(
140                "Input file is a TOC (AiFS) rather than an archive."
141            ));
142        }
143
144        let path = Path::new(filename);
145        let (toc_path, arc_index) = locate_toc_file(path).context("Failed to locate TOC file.")?;
146
147        let archive_size = (&mut reader)
148            .stream_length()
149            .context("Failed to determine archive size.")?;
150
151        let entries = parse_toc_entries(&toc_path, arc_index, archive_size)
152            .with_context(|| format!("Failed to parse TOC '{}'.", toc_path.display()))?;
153
154        Ok(Self {
155            reader: Arc::new(Mutex::new(reader)),
156            entries,
157            _mark: std::marker::PhantomData,
158        })
159    }
160}
161
162impl<'b, T: Read + Seek + Debug + Send + Sync + 'b> Script for ExHibitGrpArchive<'b, T> {
163    fn default_output_script_type(&self) -> OutputScriptType {
164        OutputScriptType::Json
165    }
166
167    fn default_format_type(&self) -> FormatOptions {
168        FormatOptions::None
169    }
170
171    fn is_archive(&self) -> bool {
172        true
173    }
174
175    fn iter_archive_filename<'a>(
176        &'a self,
177    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
178        Ok(Box::new(
179            self.entries.iter().map(|entry| Ok(entry.name.clone())),
180        ))
181    }
182
183    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
184        Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
185    }
186
187    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
188        if index >= self.entries.len() {
189            return Err(anyhow::anyhow!(
190                "Index out of bounds: {} (max: {}).",
191                index,
192                self.entries.len()
193            ));
194        }
195        let entry = self.entries[index].clone();
196        Ok(Box::new(GrpEntry::new(entry, self.reader.clone())))
197    }
198}
199
200#[derive(Debug)]
201struct GrpEntry<T: Read + Seek> {
202    info: GrpFileEntry,
203    reader: Arc<Mutex<T>>,
204    pos: u64,
205}
206
207impl<T: Read + Seek> GrpEntry<T> {
208    fn new(info: GrpFileEntry, reader: Arc<Mutex<T>>) -> Self {
209        Self {
210            info,
211            reader,
212            pos: 0,
213        }
214    }
215
216    fn remaining(&self) -> u64 {
217        self.info.size.saturating_sub(self.pos)
218    }
219}
220
221impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for GrpEntry<T> {
222    fn name(&self) -> &str {
223        &self.info.name
224    }
225
226    fn size(&self) -> Option<u64> {
227        Some(self.info.size)
228    }
229
230    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
231        Ok(Box::new(self))
232    }
233}
234
235impl<T: Read + Seek> Read for GrpEntry<T> {
236    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
237        if buf.is_empty() || self.pos >= self.info.size {
238            return Ok(0);
239        }
240        let remaining = self.remaining() as usize;
241        if remaining == 0 {
242            return Ok(0);
243        }
244        let to_read = buf.len().min(remaining);
245        let mut reader = self.reader.lock().map_err(|e| {
246            std::io::Error::new(
247                std::io::ErrorKind::Other,
248                format!("Failed to lock reader mutex: {}", e),
249            )
250        })?;
251        reader.seek(SeekFrom::Start(self.info.offset + self.pos))?;
252        let bytes = reader.read(&mut buf[..to_read])?;
253        self.pos = self.pos.checked_add(bytes as u64).ok_or_else(|| {
254            std::io::Error::new(std::io::ErrorKind::Other, "Read position overflow.")
255        })?;
256        Ok(bytes)
257    }
258}
259
260impl<T: Read + Seek> Seek for GrpEntry<T> {
261    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
262        let new_pos = match pos {
263            SeekFrom::Start(offset) => offset,
264            SeekFrom::End(offset) => {
265                let signed = self.info.size as i128 + offset as i128;
266                if signed < 0 {
267                    return Err(std::io::Error::new(
268                        std::io::ErrorKind::InvalidInput,
269                        "Seek before entry start is not allowed.",
270                    ));
271                }
272                signed as u64
273            }
274            SeekFrom::Current(offset) => {
275                let signed = self.pos as i128 + offset as i128;
276                if signed < 0 {
277                    return Err(std::io::Error::new(
278                        std::io::ErrorKind::InvalidInput,
279                        "Seek before entry start is not allowed.",
280                    ));
281                }
282                signed as u64
283            }
284        };
285        if new_pos > self.info.size {
286            return Err(std::io::Error::new(
287                std::io::ErrorKind::InvalidInput,
288                "Seek beyond entry size is not allowed.",
289            ));
290        }
291        self.pos = new_pos;
292        Ok(self.pos)
293    }
294}
295
296#[derive(Debug)]
297struct NameInfo {
298    digits_offset: usize,
299    digits_len: usize,
300    arc_num: u32,
301}
302
303fn matches_grp_name(filename: &str) -> bool {
304    Path::new(filename)
305        .file_name()
306        .and_then(|name| name.to_str())
307        .and_then(|name| parse_name_info(name).ok())
308        .is_some()
309}
310
311fn parse_name_info(name: &str) -> Result<NameInfo> {
312    if name.len() < 7 {
313        return Err(anyhow::anyhow!(
314            "Filename '{}' is too short for GRP pattern.",
315            name
316        ));
317    }
318    let name = name.as_bytes();
319    let prefix = &name[..3];
320    if !prefix.eq_ignore_ascii_case(b"res") {
321        return Err(anyhow::anyhow!(
322            "Filename '{:#?}' does not start with 'res'.",
323            name
324        ));
325    }
326    let suffix = &name[name.len() - 4..];
327    if !suffix.eq_ignore_ascii_case(b".grp") {
328        return Err(anyhow::anyhow!(
329            "Filename '{:#?}' does not end with '.grp'.",
330            name
331        ));
332    }
333    let digits = &name[3..name.len() - 4];
334    if digits.is_empty() || !digits.iter().all(|c| c.is_ascii_digit()) {
335        return Err(anyhow::anyhow!(
336            "Filename '{:#?}' does not contain a numeric sequence.",
337            name
338        ));
339    }
340    let arc_num = std::str::from_utf8(digits)
341        .with_context(|| {
342            format!(
343                "Failed to parse archive number from '{:#?}' (digits '{:#?}').",
344                name, digits
345            )
346        })?
347        .parse::<u32>()
348        .with_context(|| {
349            format!(
350                "Failed to parse archive number from '{:#?}' (digits '{:#?}').",
351                name, digits
352            )
353        })?;
354    Ok(NameInfo {
355        digits_offset: 3,
356        digits_len: digits.len(),
357        arc_num,
358    })
359}
360
361fn locate_toc_file(path: &Path) -> Result<(PathBuf, u32)> {
362    let file_name = path
363        .file_name()
364        .and_then(|name| name.to_str())
365        .ok_or_else(|| anyhow::anyhow!("Filename contains invalid UTF-8."))?;
366    let info = parse_name_info(file_name)?;
367    if info.arc_num == 0 {
368        return Err(anyhow::anyhow!(
369            "Archive '{}' has number 0 and therefore no preceding TOC file.",
370            file_name
371        ));
372    }
373
374    let mut toc_num = info.arc_num as i64 - 1;
375    let mut arc_index: u32 = 1;
376    while toc_num >= 0 {
377        let digits = format!("{:0width$}", toc_num, width = info.digits_len);
378        let mut candidate = String::with_capacity(file_name.len());
379        candidate.push_str(&file_name[..info.digits_offset]);
380        candidate.push_str(&digits);
381        candidate.push_str(&file_name[info.digits_offset + info.digits_len..]);
382        let candidate_path = path.with_file_name(&candidate);
383        if !candidate_path.exists() {
384            return Err(anyhow::anyhow!(
385                "TOC file '{}' does not exist.",
386                candidate_path.display()
387            ));
388        }
389        let mut file = std::fs::File::open(&candidate_path).with_context(|| {
390            format!(
391                "Failed to open TOC candidate '{}'.",
392                candidate_path.display()
393            )
394        })?;
395        let mut header = [0u8; 4];
396        file.read_exact(&mut header).with_context(|| {
397            format!("Failed to read header from '{}'.", candidate_path.display())
398        })?;
399        if &header == b"AiFS" {
400            return Ok((candidate_path, arc_index));
401        }
402        toc_num -= 1;
403        arc_index = arc_index
404            .checked_add(1)
405            .ok_or_else(|| anyhow::anyhow!("Archive index overflow while searching TOC."))?;
406    }
407
408    Err(anyhow::anyhow!(
409        "Unable to locate a TOC (AiFS) file for '{}'.",
410        file_name
411    ))
412}
413
414fn parse_toc_entries(
415    toc_path: &Path,
416    arc_index: u32,
417    archive_size: u64,
418) -> Result<Vec<GrpFileEntry>> {
419    let file = std::fs::File::open(toc_path)?;
420    let mut reader = std::io::BufReader::new(file);
421    let toc_len = reader.stream_length()?;
422    if toc_len < 0x10 {
423        return Err(anyhow::anyhow!("TOC file is too small."));
424    }
425
426    reader.seek(SeekFrom::Start(0xC))?;
427    let res_count = reader.read_i32()?;
428    if res_count <= 0 {
429        return Err(anyhow::anyhow!("TOC resource count is invalid."));
430    }
431    if arc_index as i64 > res_count as i64 {
432        return Err(anyhow::anyhow!(
433            "Archive index {} is out of range (resource count {}).",
434            arc_index,
435            res_count
436        ));
437    }
438
439    let mut index_offset = 0x10u64;
440    let mut arc_offset = None;
441    for _ in 0..res_count {
442        if index_offset + 0x10 > toc_len {
443            break;
444        }
445        reader.seek(SeekFrom::Start(index_offset))?;
446        let mut num = reader.read_i32()?;
447        if num == 0x0100_0000 {
448            index_offset = index_offset
449                .checked_add(4)
450                .ok_or_else(|| anyhow::anyhow!("Index offset overflow."))?;
451            if index_offset + 4 > toc_len {
452                break;
453            }
454            reader.seek(SeekFrom::Start(index_offset))?;
455            num = reader.read_i32()?;
456        }
457        reader.seek(SeekFrom::Start(index_offset + 0xC))?;
458        let entry_count = reader.read_u32()?;
459        if num == arc_index as i32 {
460            arc_offset = Some(index_offset);
461            break;
462        }
463        let step = (entry_count as u64)
464            .checked_mul(8)
465            .and_then(|v| v.checked_add(0x10))
466            .ok_or_else(|| anyhow::anyhow!("Index offset overflow while skipping entries."))?;
467        index_offset = index_offset
468            .checked_add(step)
469            .ok_or_else(|| anyhow::anyhow!("Index offset overflow while iterating."))?;
470    }
471
472    let arc_offset =
473        arc_offset.ok_or_else(|| anyhow::anyhow!("Archive reference not found in TOC."))?;
474
475    reader.seek(SeekFrom::Start(arc_offset + 4))?;
476    let start_index = reader.read_i32()?;
477    if start_index < 0 {
478        return Err(anyhow::anyhow!("Start index is negative."));
479    }
480    reader.seek(SeekFrom::Start(arc_offset + 0xC))?;
481    let entry_count = reader.read_i32()?;
482    if entry_count < 0 {
483        return Err(anyhow::anyhow!("Entry count is negative."));
484    }
485    let entry_count = entry_count as u32;
486
487    let data_offset = arc_offset
488        .checked_add(0x10)
489        .ok_or_else(|| anyhow::anyhow!("Entry table offset overflow."))?;
490    let table_len = (entry_count as u64)
491        .checked_mul(8)
492        .ok_or_else(|| anyhow::anyhow!("Entry table size overflow."))?;
493    if data_offset + table_len > toc_len {
494        return Err(anyhow::anyhow!("TOC entry table exceeds file size."));
495    }
496
497    let mut entries = Vec::with_capacity(entry_count as usize);
498    let mut entry_offset = data_offset;
499    for i in 0..entry_count {
500        reader.seek(SeekFrom::Start(entry_offset))?;
501        let offset = reader.read_u32()? as u64;
502        let size = reader.read_u32()? as u64;
503        if size != 0 {
504            let end = offset
505                .checked_add(size)
506                .ok_or_else(|| anyhow::anyhow!("Entry size overflow."))?;
507            if end > archive_size {
508                return Err(anyhow::anyhow!(
509                    "Entry {} exceeds archive size (offset {}, size {}).",
510                    i,
511                    offset,
512                    size
513                ));
514            }
515            let index = (start_index as u32)
516                .checked_add(i)
517                .ok_or_else(|| anyhow::anyhow!("Entry index overflow."))?;
518            entries.push(GrpFileEntry {
519                name: format!("{:05}.ogg", index),
520                offset,
521                size,
522            });
523        }
524        entry_offset += 8;
525    }
526
527    if entries.is_empty() {
528        return Err(anyhow::anyhow!("Archive contains no entries."));
529    }
530
531    Ok(entries)
532}